<?php 
/**
 * 身份证、人脸对比验证
 * 官方文档地址：https://faceid.com/pages/documents
 * 
 * 2018-01-11
 * 
 * @param int  QualifiedVal 照片与公安对比匹配度的最低合格值【选】
 */
namespace Org\Util;

class Authentication 
{
	private $IdentityJust  = '';    // 身份证正面图地址
	private $IdentityBack  = '';    // 身份证反面图地址
	private $UserFaceJust  = '';    // 人手持身份证正面的照片
	private $UserFaceBack  = '';    // 人手持身份证反面的照片
	private $IdentityInfo  = [];    // 身份证信息
	// private $IdentityAxis = [];  // 身份证中的人脸坐标轴 0=>[x,y]左上 1=>[x,y]右上 2=>[x,y]左下 3=>[x,y]右下
	private $QualifiedVal  = 70;    // 人脸对比合格值 default 70
	private $AppFaceKey    = '';    // 
	private $AppFaceSecret = '';    // 

	/**
	 * 构造函数
	 */
	public function __construct($QualifiedVal=false) {
		$this->AppFaceKey    = C('FACEID_KEY');
		$this->AppFaceSecret = C('FACEID_SECRET');
		// $this->IdentityJust  = empty($Image['IdentityJust']) ? $this->IdentityJust : $Image['IdentityJust'];
		// $this->IdentityBack  = empty($Image['IdentityBack']) ? $this->IdentityBack : $Image['IdentityBack'];
		// $this->UserFaceJust  = empty($Image['UserFaceJust']) ? $this->UserFaceJust : $Image['UserFaceJust'];
		// $this->UserFaceBack  = empty($Image['UserFaceBack']) ? $this->UserFaceBack : $Image['UserFaceBack'];
		$this->QualifiedVal  = empty($QualifiedVal) ? $this->QualifiedVal : $QualifiedVal;
	}

	/**
	 * 模拟post请求
	 * @param string url 请求的URL地址
	 * @param array post_data 请求的参数 'param key'=>'param value',
	 * @param bool httpstatus 是否返回请求的http状态码
	 * @return 请求结果
	 */
	private function HttpPost($url,$post_data,$httpstatus=false) {
		$ch = curl_init();
		// TRUE 禁用 @ 前缀在 CURLOPT_POSTFIELDS 中发送文件。 意味着 @ 可以在字段中安全得使用了。 可使用 CURLFile 作为上传的代替。
		curl_setopt($ch, CURLOPT_SAFE_UPLOAD, true);
		// 需要获取的 URL 地址，也可以在curl_init() 初始化会话的时候。
	    curl_setopt($ch, CURLOPT_URL, $url);
	    // TRUE 将curl_exec()获取的信息以字符串返回，而不是直接输出。
	    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
	    // FALSE 禁止 cURL 验证对等证书（peer's certificate）。要验证的交换证书可以在 CURLOPT_CAINFO 选项中设置，或在 CURLOPT_CAPATH中设置证书目录。
	    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
	    // 	TRUE 时会发送 POST 请求，类型为：application/x-www-form-urlencoded，是 HTML 表单提交时最常见的一种。
	    curl_setopt($ch, CURLOPT_POST, 1);
	    // 全部数据使用HTTP协议中的 "POST" 操作来发送。 要发送文件，在文件名前面加上@前缀并使用完整路径。 文件类型可在文件名后以 ';type=mimetype' 的格式指定。 这个参数可以是 urlencoded 后的字符串，类似'para1=val1&para2=val2&...'，也可以使用一个以字段名为键值，字段数据为值的数组。 如果value是一个数组，Content-Type头将会被设置成multipart/form-dat		a。 从 PHP 5.2.0 开始，使用 @ 前缀传递文件时，value 必须是个数组。 从 PHP 5.5.0 开始, @ 前缀已被废弃，文件可通过 CURLFile 发送。 设置 CURLOPT_SAFE_UPLOAD 为 TRUE 可禁用 @ 前缀发送文件，以增加安全性。
	    curl_setopt($ch, CURLOPT_POSTFIELDS, $post_data);
	    $output = curl_exec($ch);
	    // 返回获得的数据
	    if($httpstatus){
	    	// 获取返回结果的http状态
	    	$httpCode = curl_getinfo($ch,CURLINFO_HTTP_CODE);
	    	curl_close($ch);
	    	return ['response'=>$output,'httpcode'=>$httpCode];
	    }else{
	    	curl_close($ch);
	    	return $output;
	    }
	}

	/**
	 * 模拟get请求
	 * @param string url 请求的URL地址
	 * @return 请求结果
	 */
	private function HttpGet($url) {
		$curl = curl_init();
	    curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
	    curl_setopt($curl, CURLOPT_TIMEOUT, 500);
	    curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
	    curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false);
	    curl_setopt($curl, CURLOPT_URL, $url);
	    $output = curl_exec($curl);
	    curl_close($curl);
	    return $output;
	}

	/**
	 * 图片转 curl 参数对象
	 * @param string ImgPath 需要转对象的图片路径
	 */
	public function CurlEnObject($ImgPath) {
		if(!$ImgPath||!file_exists($ImgPath)) return false;
		$img = new \CurlFile($ImgPath);
		if($img){
			return $img;
		}else{
			return false;
		}
	}

	/**
	 * 身份证正面信息识别
	 * @param string ImgPath 用户上传的身份证正面图
	 * @return Array 返回识别的身份证中的信息
	 */
	public function IdentityJustInfo($ImgPath) {
		if(!$ImgPath) return false;
		$identity_post_A = [
	      'api_key'   => $this->AppFaceKey,
	      'api_secret'=> $this->AppFaceSecret,
	      'image'     => $this->CurlEnObject($ImgPath),
	      'legality'=>'1',
	    ];
	    $InterfaceUrl = 'https://api.faceid.com/faceid/v1/ocridcard';
	    $response = $this->HttpPost($InterfaceUrl,$identity_post_A);
	    $response = json_decode($response,true);
	    if($response && $response['side'] == 'front'){
	    	return $response;
	    }else{
	    	return false;
	    }
	}

	/**
	 * 身份证反面信息识别
	 * @param string ImgPath 用户上传的身份证反面图
	 * @return Array 返回识别的身份证中的信息
	 */
	public function IdentityBackInfo($ImgPath) {
		if(!$ImgPath) return false;
		$identity_post_B = [
	      'api_key'   => $this->AppFaceKey,
	      'api_secret'=> $this->AppFaceSecret,
	      'image'     => $this->CurlEnObject($ImgPath),
	      'legality'=>'1',
	    ];
	    $InterfaceUrl = 'https://api.faceid.com/faceid/v1/ocridcard';
	    $response = $this->HttpPost($InterfaceUrl,$identity_post_B);
	    $response = json_decode($response,true);
	    if($response && $response['side'] == 'back' && !empty($response['issued_by'])){
	    	return $response;
	    }else{
	    	return false;
	    }

	}

	/**
	 * 身份证中的人脸与公安数据对比
	 * @param string ImgPath IdentityHead方法裁剪的头像路径
	 * @param Array IdentityInfo IdentityJustInfo方法返回的身份证信息 
	 * @return Array 返回与公安数据对比的结果信息
	 */
	public function IdentityFace($ImgPath,$IdentityInfo) {
		if(!$ImgPath||!$IdentityInfo) return false;
	    $InterfaceUrl = 'https://api.megvii.com/faceid/v2/verify';
	    $response_n = $this->HttpPost($InterfaceUrl,[
	      'api_key'               => $this->AppFaceKey,
	      'api_secret'            => $this->AppFaceSecret,
	      'comparison_type'       => '1',
	      'face_image_type'       => 'raw_image',
	      'idcard_name'           => $IdentityInfo['name'],
	      'idcard_number'         => $IdentityInfo['id_card_number'],
	      'image'                 => $this->CurlEnObject($ImgPath),
	      'face_quality_threshold'=> '0',
	    ]);
	    $response = json_decode($response_n,true);
	    if(isset($response['error_message'])){
	        return ['error'=>'1','error_message'=>$response['error_message']];
	    }else if($response['result_faceid']['confidence']<$response['result_faceid']['thresholds']['1e-5']){
	        // 图片中的人脸与公安记录人脸对比匹配度较低！
	    	return ['error'=>'2','confidence'=>$response['result_faceid']['confidence']];//人脸身份识别度较低
	    }else if($response['result_faceid']['confidence']>=$response['result_faceid']['thresholds']['1e-5']){
	        // 人脸对比匹配度较高！
	        return ['success'=>'OK','data'=>$response];
	    }else{//系统问题
	    	return false;
	    }
	}
	
	/**
	 * 裁剪身份证中的头像
	 * @param  string   ImgPath  用户上传的身份证正面照片
	 * @param  Array IdentityInfo  IdentityJustInfo方法返回的身份证信息 ['head_rect']
	 * @param  string   path  裁剪后的头像存储位置
	 * @return Array 返回裁剪后的图片名称
	 */
	public function IdentityHead($ImgPath,$IdentityInfo,$path) {
		if(!$ImgPath||!$IdentityInfo) return false;
		if(!is_array($IdentityInfo)){
			return false;
		}
		if(!is_dir($path)){
			mkdir($path,0777,true);
		}

		// 读取为资源类型
		$source=imagecreatefromjpeg($ImgPath);
	    list($src_w,$src_h)=getimagesize($ImgPath);
	    $src_lt_x = $src_w*$IdentityInfo['lt']['x'];
	    $src_lt_y = $src_h*$IdentityInfo['lt']['y'];
	    $src_rt_x = $src_w*$IdentityInfo['rt']['x'];
	    $src_lb_y = $src_h*$IdentityInfo['lb']['y'];
	    $w = $src_rt_x-$src_lt_x;
	    $h = $src_lb_y-$src_lt_y;
	    // 剪裁
	    $croped=imagecreatetruecolor($w,$h);
	    imagecopy($croped, $source, 0, 0, $src_lt_x, $src_lt_y, $w, $h);
	    $path = rtrim($path,'/').'/';
	    do{
	    	$filename = time().rand(1000,9999).'.jpg';
	    }while (file_exists($path.$filename));
	    $head = imagejpeg($croped,$path.$filename,95);
	    if(file_exists($path.$filename)){
	    	chmod($path.$filename, 0777);
	    	return $filename;
	    }else{
	    	return false;
	    }
	}

	/**
	 * 识别用户现场的图片与公安数据进行对比
	 * @param  string  ImgPath 用户上传的图片
	 * @param  Array  IdentityInfo IdentityJustInfo 方法返回的身份证信息
	 * @return Array 返回与公安数据进行对比的结果信息
	 */
	public function UserHead($ImgPath,$IdentityInfo) {
		if(!$ImgPath||!$IdentityInfo) return false;
		$identityface_post = [
	      'api_key'               => $this->AppFaceKey,
	      'api_secret'            => $this->AppFaceSecret,
	      'comparison_type'       => '1',
	      'face_image_type'       => 'raw_image',
	      'idcard_name'           => $IdentityInfo['name'],
	      'idcard_number'         => $IdentityInfo['id_card_number'],
	      'image'                 => $this->CurlEnObject($ImgPath),
	    ];
	    $InterfaceUrl = 'https://api.megvii.com/faceid/v2/verify';
	    $identityface_data = $this->HttpPost($InterfaceUrl,$identityface_post);
	    $identityface_deData = json_decode($identityface_data,true);
	    //用户没有按照指定格式上传证件信息
	    if(isset($identityface_deData['error_message'])){
	      return ['error'=>'1','error_message'=>$identityface_deData['error_message'],'errornum'=>$identityface_deData['error_message']];

	    }else if($identityface_deData['result_faceid']['confidence']<$identityface_deData['result_faceid']['thresholds']['1e-5']){
	        // 图片中的人脸与公安记录人脸对比匹配度较低！
	    	return ['error'=>'2','confidence'=>$identityface_deData['result_faceid']['confidence']];//人脸身份识别度较低
	    }else if($identityface_deData['result_faceid']['confidence']>=$identityface_deData['result_faceid']['thresholds']['1e-5']){
	      // 人脸对比匹配度较高！
	      return ['success'=>'OK','data'=>$identityface_deData];
	    }else{//系统问题
	    	return false;
	    }
	}

	/**
	 * 筋斗车身份认证方法
	 * 2018-01-12
	 * @param Array Image 【2选1】身份证的正反面[IdentityJust,IdentityBack]、【1】人的上身照[UserFace]
	 */
	public function JdcAuth($ImgPath) {

		$IdentityJust = $ImgPath['IdentityJust'];// 身份证正面图片信息
		$IdentityBack = $ImgPath['IdentityBack'];// 身份证反面图片信息
		$UserFaceJust = $ImgPath['UserFaceJust'];// 用户现场人脸图片
		// 获取身份证正面信息
		$IdentityJustInfo_result = $this->IdentityJustInfo($IdentityJust);
		if(!$IdentityJustInfo_result){
			return ['error'=>1,'message'=>"ERROR：（505）\n身份证信息获取失败！"];
		}else if(@$IdentityJustInfo_result['error']){
			return ['error'=>1,'message'=>"身份证信息获取失败！\nERROR：".$IdentityJustInfo_result['error']];
		}
       ;
		// 获取身份证背面信息
		$IdentityBackInfo_result = $this->IdentityBackInfo($IdentityBack);
		if(!$IdentityBackInfo_result){
			return ['error'=>1,'message'=>'反面身份证信息获取失败！'];
		}else if(@$IdentityBackInfo_result['error']){
			return ['error'=>1,'message'=>"反面身份证信息获取失败！\nERROR：".$IdentityBackInfo_result['error']];
		}

		// 截取身份证头像
		$headpath = C('PUBLIC_ROUTE').'/Public/Uploads/identifyhead/'.date('Ymd').'/';

		$IdentityHead_result = $this->IdentityHead($IdentityJust,$IdentityJustInfo_result['head_rect'],$headpath);
		if(!$IdentityHead_result){
			return ['error'=>2,'message'=>'身份证人脸对比失败！'];
		}

		// 验证身份证中的头像识别度
		$IdentityFace_result = $this->IdentityFace($headpath.$IdentityHead_result,$IdentityJustInfo_result);
		if(isset($IdentityFace_result['error'])&&$IdentityFace_result['error']=='1'){
			switch ($IdentityFace_result['error_message']) {
				case 'MISSING_ARGUMENTS: comparison_type':
				case 'MISSING_ARGUMENTS: face_image_type':
				case 'MISSING_ARGUMENTS: idcard_number':
				case 'MISSING_ARGUMENTS: image':
					return ['error'=>5,'message'=>'缺少系统参数，请联系客服处理！'];

				case 'IMAGE_ERROR_UNSUPPORTED_FORMAT':
					return ['error'=>5,'message'=>'您上传的可能不是一张有效的照片！'];

				case 'NO_FACE_FOUND: image':
					return ['error'=>5,'message'=>'没有识别到身份证中的头像！'];

				case 'AUTHORIZATION_ERROR':
					return ['error'=>5,'message'=>'系统参数有误，请联系客服处理！'];

				case 'CONCURRENCY_LIMIT_EXCEEDED':
					return ['error'=>5,'message'=>'系统繁忙，请稍后再试！'];

				case 'INTERNAL_ERROR':
					return ['error'=>5,'message'=>'系统错误，请联系客服处理！'];

				default:
					return ['error'=>5,'message'=>'请按照指定格式上传证件信息图片！'];
			}
		}else if(isset($IdentityFace_result['error'])&&$IdentityFace_result['error']=='2'){
			return ['error'=>3,'message'=>"ERROR：（400） \n 经过与证件照对比，照片中人脸的可信度较低。"];
		}else if(!$IdentityFace_result){
			return ['error'=>2,'message'=>"ERROR：（401） \n身份证人脸对比失败！"];
		}

		// 验证用户的个人上身照
		$UserHead_result = $this->UserHead($UserFaceJust,$IdentityJustInfo_result);
		
		if(isset($UserHead_result['error'])&&$UserHead_result['error']=='1'){
			switch ($UserHead_result['error_message']) {
				case 'IMAGE_ERROR_UNSUPPORTED_FORMAT: image':
					return ['error'=>5,'message'=>"ERROR：（403） \n您上传的可能不是一张有效的照片！"];

				case 'MULTIPLE_FACES: image':
					return ['error'=>5,'message'=>"ERROR：（405） \n上身照只能出现一张人脸,请核对照片！"];

				case 'INVALID_IMAGE_SIZE: image':
					return ['error'=>5,'message'=>"ERROR：（406） \n请上传2M以内，长和宽在4096像素之内的照片。"];

				case 'NO_FACE_FOUND: image':
					return ['error'=>5,'message'=>"ERROR：（407） \n抱歉！照片中未识别到人脸。"];

				case 'LOW_QUALITY':
					return ['error'=>5,'message'=>"ERROR：（408） \n经过与证件照对比，照片中人脸的可信度较低。"];

				default:
					return ['error'=>5,'message'=>"ERROR：（409） \n请按照指定格式上传证件信息图片！"];
			}
		}else if(isset($UserHead_result['error'])&&$UserHead_result['error']=='2'){
			return ['error'=>3,'message'=>"ERROR：（409） \n经过与证件照对比，照片中人脸的可信度较低。"];
		}else if(!$UserHead_result){
			return ['error'=>3,'message'=>"ERROR：（409） \n个人上身照片没有通过审核条件！"];
		}else if($UserHead_result['success']=='OK'){
			return [
				'success'=>'OK',
				'data'=>[
					'IdentityJustInfo'=>$IdentityJustInfo_result,
					'IdentityBackInfo'=>$IdentityBackInfo_result,
					'UserHead'=>$UserHead_result,
					'IdentityFace'=>$IdentityFace_result['data'],
				],
				'message'=>'验证成功！'
			];
		}
	}

	
	/**
	 * 获取身份证信息
	 */
	public function IdentityAuth($ImgPath){
		$IdentityJust = $ImgPath['IdentityJust'];// 身份证正面图片信息
		$IdentityBack = $ImgPath['IdentityBack'];// 身份证反面图片信息
		// 获取身份证正面信息
		$IdentityJustInfo_result = $this->IdentityJustInfo($IdentityJust);
		if(!$IdentityJustInfo_result){
			return ['error'=>1,'message'=>"ERROR：（505）\n身份证信息获取失败！"];
		}else if(@$IdentityJustInfo_result['error']){
			return ['error'=>1,'message'=>"身份证信息获取失败！\nERROR：".$IdentityJustInfo_result['error']];
		}

		// 获取身份证背面信息
		$IdentityBackInfo_result = $this->IdentityBackInfo($IdentityBack);
		if(!$IdentityBackInfo_result){
			return ['error'=>1,'message'=>'反面身份证信息获取失败！'];
		}else if(@$IdentityBackInfo_result['error']){
			return ['error'=>1,'message'=>"反面身份证信息获取失败！\nERROR：".$IdentityBackInfo_result['error']];
		}
		return [
				'success'=>'OK',
				'data'=>[
					'IdentityJustInfo'=>$IdentityJustInfo_result,
					'IdentityBackInfo'=>$IdentityBackInfo_result,
				],
				'message'=>'验证成功！'
			];
	}

	/**
	 * 筋斗车风控验证消息
	 * 2018-01-12
	 * @param Array Image 【2选1】身份证的正反面[IdentityJust,IdentityBack]、【1】人的上身照[UserFace]
	 */
	public function IdentityAuthRisk($ImgPath) {

		$IdentityJust = $ImgPath['IdentityJust'];// 身份证正面图片信息
		$UserFaceJust = $ImgPath['UserFaceJust'];// 用户现场人脸图片
		// 获取身份证正面信息
		$IdentityJustInfo_result = $this->IdentityJustInfo($IdentityJust);
		if(!$IdentityJustInfo_result){
			return ['error'=>1,'message'=>"ERROR：（505）\n身份证信息获取失败！"];
		}else if(@$IdentityJustInfo_result['error']){
			return ['error'=>1,'message'=>"身份证信息获取失败！\nERROR：".$IdentityJustInfo_result['error']];
		}

		

		// 截取身份证头像
		$headpath = C('PUBLIC_ROUTE').'/Public/Uploads/identifyhead/'.date('Ymd').'/';
		$IdentityHead_result = $this->IdentityHead($IdentityJust,$IdentityJustInfo_result['head_rect'],$headpath);
		if(!$IdentityHead_result){
			return ['error'=>2,'message'=>'身份证人脸对比失败！'];
		}

		// 验证身份证中的头像识别度
		$IdentityFace_result = $this->IdentityFace($headpath.$IdentityHead_result,$IdentityJustInfo_result);
		if(isset($IdentityFace_result['error'])&&$IdentityFace_result['error']=='1'){
			@unlink($headpath.$IdentityHead_result);
			switch ($IdentityFace_result['error_message']) {
				case 'MISSING_ARGUMENTS: comparison_type':
				case 'MISSING_ARGUMENTS: face_image_type':
				case 'MISSING_ARGUMENTS: idcard_number':
				case 'MISSING_ARGUMENTS: image':
					return ['error'=>5,'message'=>'缺少系统参数，请联系客服处理！'];

				case 'IMAGE_ERROR_UNSUPPORTED_FORMAT':
					return ['error'=>5,'message'=>'您上传的可能不是一张有效的照片！'];

				case 'NO_FACE_FOUND: image':
					return ['error'=>5,'message'=>'没有识别到身份证中的头像！'];

				case 'AUTHORIZATION_ERROR':
					return ['error'=>5,'message'=>'系统参数有误，请联系客服处理！'];

				case 'CONCURRENCY_LIMIT_EXCEEDED':
					return ['error'=>5,'message'=>'系统繁忙，请稍后再试！'];

				case 'INTERNAL_ERROR':
					return ['error'=>5,'message'=>'系统错误，请联系客服处理！'];
				default:
					return ['error'=>5,'message'=>'请按照指定格式上传证件信息图片！'];
			}
		}else if(isset($IdentityFace_result['error'])&&$IdentityFace_result['error']=='2'){
			return ['error'=>3,'message'=>"ERROR：（400） \n 经过与证件照对比，照片中人脸的可信度较低。"];
		}else if(!$IdentityFace_result){
			return ['error'=>2,'message'=>"ERROR：（401） \n 身份证人脸对比失败！"];
		}

		// 验证用户的个人上身照
		$UserHead_result = $this->UserHead($UserFaceJust,$IdentityJustInfo_result);
		
		if(isset($UserHead_result['error'])&&$UserHead_result['error']=='1'){
			switch ($UserHead_result['error_message']) {
				case 'IMAGE_ERROR_UNSUPPORTED_FORMAT: image':
					return ['error'=>5,'message'=>"ERROR：（403） \n您上传的可能不是一张有效的照片！"];

				case 'MULTIPLE_FACES: image':
					return ['error'=>5,'message'=>"ERROR：（405） \n上身照只能出现一张人脸,请核对照片！"];

				case 'INVALID_IMAGE_SIZE: image':
					return ['error'=>5,'message'=>"ERROR：（406） \n请上传2M以内，长和宽在4096像素之内的照片。"];

				case 'NO_FACE_FOUND: image':
					return ['error'=>5,'message'=>"ERROR：（407） \n抱歉！照片中未识别到人脸。"];

				case 'LOW_QUALITY':
					return ['error'=>5,'message'=>"ERROR：（408） \n经过与证件照对比，照片中人脸的可信度较低。"];

				default:
					return ['error'=>5,'message'=>"ERROR：（409） \n请按照指定格式上传证件信息图片！"];
			}
		}else if(isset($UserHead_result['error'])&&$UserHead_result['error']=='2'){
			return ['error'=>3,'message'=>"ERROR：（409） \n经过与证件照对比，照片中人脸的可信度较低。"];
		}else if(!$UserHead_result){
			return ['error'=>3,'message'=>"ERROR：（409） \n个人上身照片没有通过审核条件！"];
		}else if($UserHead_result['success']=='OK'){
			return [
				'success'=>'OK',
				'data'=>[
					'IdentityJustInfo'=>$IdentityJustInfo_result,
					'UserHead'=>$UserHead_result,
					'IdentityFace'=>$IdentityFace_result['data'],
				],
				'message'=>'经过与证件照对比,通过验证！'
			];
		}
	}
}